昨天我們完成了藍芽的基本環境設定,並建立了一個簡單的 BluetoothService 架構。
今天要繼續進化,讓我們的 App 能夠:
首先,我們定義一個 BluetoothServiceDelegate 協議,讓 Service 可以把 掃描到的裝置 和 接收到的資料 傳回 UI。
protocol BluetoothServiceDelegate: AnyObject {
    func getBLEPeripherals(peripherals: [CBPeripheral])   // 回傳掃描到的裝置
    func getBLEPeripheralsValue(value: String)            // 回傳接收到的數據
}
透過這種設計,我們能讓 Service 與 UI 完全解耦。
UI 只要實作這兩個方法,就能即時取得藍芽掃描結果與資料更新。
接著,我們建立 BluetoothService 類別。
這裡採用 單例模式 (Singleton),確保全專案只存在一個藍芽實例,方便集中管理。
class BluetoothService: NSObject {
    
    static let shared = BluetoothService()
    weak var delegate: BluetoothServiceDelegate?
    
    var central: CBCentralManager?            // 中央管理器
    var peripheral: CBPeripheralManager?      // 外圍管理器
    var connectedPeripheral: CBPeripheral?    // 已連線裝置
    var rxtxCharacteristic: CBCharacteristic? // 資料收發特徵值
    
    private var bluePeripherals: [CBPeripheral] = []
    
    private override init() {
        super.init()
        let queue = DispatchQueue.global()
        central = CBCentralManager(delegate: self, queue: queue)
        peripheral = CBPeripheralManager(delegate: self, queue: queue)
    }
}
這邊同時初始化了 中央管理器 (Central) 與 外圍管理器 (Peripheral)。
目前我們主要使用 Central 角色,但未來若想讓手機變成 BLE 外設,也能直接擴充。
我們接著實作幾個公開方法來控制藍芽操作:
func startScan() {
    central?.scanForPeripherals(withServices: nil, options: nil)
}
func stopScan() {
    central?.stopScan()
}
func connectPeripheral(peripheral: CBPeripheral) {
    self.connectedPeripheral = peripheral
    central?.connect(peripheral, options: nil)
}
func disconnectPeripheral(peripheral: CBPeripheral) {
    central?.cancelPeripheralConnection(peripheral)
}
這些方法負責控制藍芽掃描、連線與中斷:
當藍芽狀態變化或掃描到裝置時,CBCentralManagerDelegate 會觸發對應事件。
藍芽狀態監控:
func centralManagerDidUpdateState(_ central: CBCentralManager) {
    switch central.state {
    case .poweredOn:
        print("藍牙開啟,開始掃描")
        startScan()
    case .poweredOff:
        print("藍牙關閉")
    case .unauthorized:
        print("沒有藍牙權限")
    default:
        print("狀態: \(central.state.rawValue)")
    }
}
掃描到裝置:
func centralManager(_ central: CBCentralManager,
                    didDiscover peripheral: CBPeripheral,
                    advertisementData: [String : Any],
                    rssi RSSI: NSNumber) {
    
    guard !bluePeripherals.contains(where: { $0.name == peripheral.name }) else { return }
    
    if let name = peripheral.name {
        bluePeripherals.append(peripheral)
        print("找到裝置: \(name)")
        delegate?.getBLEPeripherals(peripherals: bluePeripherals)
    }
}
成功連線:
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
    print("已連線到:\(peripheral.name ?? "未知裝置")")
    peripheral.delegate = self
    peripheral.discoverServices(nil)
}
當成功連線後,就會進入「服務 → 特徵值 → 資料」的流程。
發現服務:
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
    guard let services = peripheral.services else { return }
    for service in services {
        print("服務: \(service)")
        peripheral.discoverCharacteristics(nil, for: service)
    }
}
發現特徵值:
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
    guard let characteristics = service.characteristics else { return }
    
    for characteristic in characteristics {
        print("特徵值: \(characteristic)")
        
        // 偵測特定 UUID (例如 FFE1)
        if characteristic.uuid.isEqual(CBUUID(string: "FFE1")) {
            rxtxCharacteristic = characteristic
            peripheral.readValue(for: characteristic)
            peripheral.setNotifyValue(true, for: characteristic)
        }
    }
}
接收到資料:
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
    guard characteristic == rxtxCharacteristic,
          let data = characteristic.value,
          let stringValue = String(data: data, encoding: .utf8) else { return }
    
    print("接收到資料: \(stringValue)")
    delegate?.getBLEPeripheralsValue(value: stringValue)
}
這樣就能讓 UI 端即時接收 BLE 資料更新 🎉
今天我們完成了整個藍芽連線流程的關鍵部分:
到這裡,我們已經擁有一個可「掃描 → 連線 → 收資料」的藍芽核心服務。
明天(Day28)我們將進一步整合 UI 介面,讓使用者能選擇藍芽裝置並查看即時資料更新!